/// ===========================================================================
/// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
/// KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
/// PURPOSE.
/// ===========================================================================
/// 
/// Project:        MOSS Faceted Search
/// Author:         Leonid Lyublinski (leonidly@microsoft.com)
/// Company:        Microsoft Services
/// Date:           05/04/2007  Version:        1.0
///
/// ===========================================================================

using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml.Serialization;
using System.Data;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using EntLib = Microsoft.Practices.EnterpriseLibrary;
using SearchAdmin = Microsoft.Office.Server.Search.Administration;

namespace Microsoft.SharePoint.Portal.ExtendedSearch.Common
{
    /// <summary>
    /// Contains utilities to parse/build querystring. Implemented as static methods.
    /// </summary>
    /// <remarks>Implementation of the faceted search relies on </remarks>
    [Microsoft.SharePoint.Security.SharePointPermission(System.Security.Permissions.SecurityAction.Demand, Impersonate = true)]
    public class Utility
    {
        #region Properties
        /// <summary>
        /// Array of operators used to build Sql search
        /// </summary>
        public static Operator[] Operators
        {
            get
            {
                Operator[] operators = new Operator[11];
                operators[0] = new Operator(0, Resource.ADVSEARCH_PROP_DEFAULT, string.Empty);
                operators[1] = new Operator(1, Resource.ADVSEARCH_PROP_CONTAIN, " LIKE ");
                operators[2] = new Operator(2, Resource.ADVSEARCH_PROP_NOTCONTAIN, " NOT LIKE ");
                operators[3] = new Operator(3, Resource.ADVSEARCH_PROP_EARLIER, " < ");
                operators[4] = new Operator(4, Resource.ADVSEARCH_PROP_LATER, " > ");
                operators[5] = new Operator(5, Resource.ADVSEARCH_PROP_LESS, " < ");
                operators[6] = new Operator(6, Resource.ADVSEARCH_PROP_GREATER, " > ");
                operators[7] = new Operator(7, Resource.ADVSEARCH_PROP_EQUAL, " = ");
                operators[8] = new Operator(8, Resource.ADVSEARCH_PROP_NOTEQUAL, " <> ");
                operators[9] = new Operator(9, Resource.ADVSEARCH_PROP_TRUE, " = TRUE ");
                operators[10] = new Operator(10, Resource.ADVSEARCH_PROP_FALSE, " = FALSE ");

                return operators;
            }
        }

        public static Scope[] CachedScopes
        {
            get
            {
                HttpApplicationState app = HttpContext.Current.Application;
                if (app.Get("CachedScopes") == null)
                {
                    SPSecurity.RunWithElevatedPrivileges(delegate()
                    {
                        SearchAdmin.Scopes scopes = new SearchAdmin.Scopes(SearchAdmin.SearchContext.Current);
                        Scope[] cachedScopes = new Scope[scopes.AllScopes.Count];
                        for (int i = 0; i < scopes.AllScopes.Count; i++)
                        {
                            SearchAdmin.Scope scope = scopes.AllScopes[i];
                            cachedScopes[i] = new Scope(scope.ID, scope.Name);
                        }
                        app.Add("CachedScopes", cachedScopes);
                    });

                }
                return (Scope[])app["CachedScopes"];
            }
        } 
        #endregion

        #region Methods
        /// <summary>
        /// Creates SearchQuery structure from the querystring or HTTP POST request
        /// </summary>
        /// <returns><seealso cref="SearchQuery"/></returns>
        /// <remarks>Parsing of querystring is based on regular expressions patterns</remarks>
        public static SearchQuery ExtractQuery()
        {
            SearchQuery query = new SearchQuery();
            query.Properties = new NameValueCollection();
            bool isAdvancedSearch = false; ;
            if (HttpContext.Current.Request.HttpMethod == "POST")
            {
                FormData formData = ExtractFormData(out isAdvancedSearch);
                query.FormData = formData;
                query.IsAdvancedSearch = isAdvancedSearch;
                query.QueryMethod = QueryMethodEnum.FullTextSqlQuery;
            }
            if (!isAdvancedSearch)
            {
                query.QueryMethod = QueryMethodEnum.KeywordQuery;
                
                // k parameter
                string k = HttpContext.Current.Request.QueryString["k"];
                if (!string.IsNullOrEmpty(k))
                {
                    // separate search word from properties by space, assuming that keyphrase is 1st
                    // look for property by construct <property name>:"<property value>", array of properties should look like
                    // xxx: yyy: zzz:.
                    // search phrase can be quoted and have a space. Space is a delimiter, yet will look positively fwd
                    // skip quoted phrases
                    int propertyStart = -1;  //                             -------VVV-------- addition
                    string propertyPattern = String.Format(Constants.REGEX_QUERYSTRING_SPLITTER, "propName", "propValue");  //@"(?:^|\s+)(?<propName>(\w+)):""(?<propValue>([^""]+))""";
                    Regex propertyRe = new Regex(propertyPattern, RegexOptions.IgnoreCase);
                    try
                    {
                        MatchCollection propertyMatches = propertyRe.Matches(k);
                        if (propertyMatches.Count > 0)
                        {
                            string[] excluded = Constants.EXCLUDED_PROPERTIES.Split(Splitter.UniDelimiter, StringSplitOptions.RemoveEmptyEntries);
                            ArrayList alExcluded = new ArrayList(excluded);

                            propertyStart = propertyMatches[0].Index;
                            foreach (Match property in propertyMatches)
                            {
                                
                                CaptureCollection captures = property.Captures;
                                string propName = property.Groups["propName"].Value;
                                if (alExcluded.Contains(propName)) continue;
                                string propValue = property.Groups["propValue"].Value;
                                // provision for multiple values for the same property
                                if (string.IsNullOrEmpty(query.Properties[propName]))
                                {
                                    query.Properties.Add(propName, propValue);
                                }
                                else if (!query.Properties[propName].Equals(propValue, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(propValue))
                                {
                                    query.Properties[propName] += Splitter.Delimiter + propValue;
                                }
                            }

                            query.Keywords = (propertyStart > 0) ? k.Substring(0, propertyStart).Trim() : string.Empty;
                        }
                        else
                            query.Keywords = k.Trim();

                    }
                    catch (Exception ex)
                    {
                        HandleException(ex, false, null, null);
                    }
                }

                // s parameter
                query.Scope = HttpContext.Current.Request.QueryString["s"];

                // v parameter
                query.View = HttpContext.Current.Request.QueryString["v"];

                // start parameter
                string start = HttpContext.Current.Request.QueryString["start"];
                query.Start = (string.IsNullOrEmpty(start) ? -1 : int.Parse(start));

                if (query.Start == -1)
                {
                    for (int i = 1; i < 7; i++)
                    {
                        string st = HttpContext.Current.Request.QueryString[string.Format("start{0}", i)];
                        if (!string.IsNullOrEmpty(st))
                        {
                            query.Start = int.Parse(st);
                            break;
                        }
                    }
                }
            }

            return query;
        }

        /// <summary>
        /// Parses POST data into the FormData structure
        /// </summary>
        /// <returns></returns>
        internal static FormData ExtractFormData(out bool isAdvancedSearch)
        {
            FormData formData = new FormData(5);
            isAdvancedSearch = false;
            if (HttpContext.Current.Request.HttpMethod == "POST")
            {
                NameValueCollection form = HttpContext.Current.Request.Form;
                ArrayList alTextTypes = (form["ASB_TextDT_Props"] != null) ? alTextTypes = new ArrayList(form["ASB_TextDT_Props"].ToLower().Split(Splitter.UniDelimiter, StringSplitOptions.RemoveEmptyEntries)) : null;
                ArrayList alDateTypes = (form["ASB_DateTimeDT_Props"] != null) ? alDateTypes = new ArrayList(form["ASB_DateTimeDT_Props"].ToLower().Split(Splitter.UniDelimiter, StringSplitOptions.RemoveEmptyEntries)) : null;

                int l = 0; int s = 0;
                foreach (string key in HttpContext.Current.Request.Form.AllKeys)
                {
                    //HttpContext.Current.Response.Write(string.Format("{0}={1}", key, form[key]));
                    //HttpContext.Current.Response.Write("<BR>");

                    #region Parsing form
                    if (key.Contains("ASB_TQS_"))
                    {
                        //Text box section query
                        if (key.Contains("ASB_TQS_AndQ_tb"))
                        {
                            formData.Keywords.AndKeywords = form[key];
                        }
                        if (key.Contains("ASB_TQS_PhraseQ_tb"))
                        {
                            formData.Keywords.PhraseKeywords = form[key];
                        }
                        if (key.Contains("ASB_TQS_OrQ_tb"))
                        {
                            formData.Keywords.OrKeywords = form[key];
                        }
                        if (key.Contains("ASB_TQS_NotQ_tb"))
                        {
                            formData.Keywords.NotKeywords = form[key];
                        }
                    }
                    else if (key.Contains("ASB_SS_"))
                    {
                        if (key.Contains("ASB_SS_lcb_"))
                        {
                            //This is a language check box input
                            string langID = key.Substring(key.LastIndexOf("_") + 1);
                            formData.Languages[l] = int.Parse(langID);
                            l++;
                        }
                        else if (key.Contains("ASB_SS_scb_"))
                        {
                            //This is scopes
                            string scopeID = key.Substring(key.LastIndexOf("_") + 1);
                            formData.Scopes[s] = new Scope(int.Parse(scopeID));
                            s++;
                        }
                    }
                    else if (key.Contains("ASB_PS_"))
                    {
                        //get logical union 
                        if (key.Contains("ASB_PS_lolb_0"))
                        {
                            formData.IsAnd = form[key].Equals(Resource.ADVSEARCH_PROP_LOGICAL_AND, StringComparison.CurrentCulture);
                        }

                        //Properties Section input
                        for (int i = 0; i < 5; i++) // won't bother to test each property
                        {
                            AdvancedProp prop = (string.IsNullOrEmpty(formData.Properties[i].PropName)) ? new AdvancedProp() : formData.Properties[i];
                            if (key.Contains(string.Concat("ASB_PS_plb_", i)))
                            {
                                prop.PropName = form[key];
                                if (alDateTypes.Contains(prop.PropName.ToLower())) prop.PropType = PropTypeEnum.DateTime;
                                else if (alTextTypes.Contains(prop.PropName.ToLower())) prop.PropType = PropTypeEnum.Text;
                                else prop.PropType = PropTypeEnum.Numeric;
                            }
                            if (key.Contains(string.Concat("ASB_PS_olb_", i)))
                            {
                                prop.PropOperator = new Operator(form[key]);
                            }
                            if (key.Contains(string.Concat("ASB_PS_pvtb_", i)))
                            {
                                prop.PropValue = form[key];
                            }
                            formData.Properties[i] = prop;
                        }
                    }
                    else if (key.Contains("ASB_ResType_formData"))
                    {
                        // extra query for e.g. documents
                        formData.ResultType = form["ASB_ResType_Query"];
                    }
                    isAdvancedSearch = !formData.IsEmpty();
                    #endregion
                }
            }
            return formData;
        }

        /// <summary>
        /// Builds a querystring by joining META properties with keyword, view, scope and start parameters.
        /// </summary>
        /// <param name="query"><see cref="SearchQuery"/></param>
        /// <returns>Returns a generated querystring <see cref="string"/> to use in response redirect</returns>
        public static string BuildQueryString(SearchQuery query)
        {
            string qs = string.Empty;
            ArrayList qsa = new ArrayList();
            if (!string.IsNullOrEmpty(query.Scope))
                qsa.Add("s=" + HttpUtility.UrlEncode(query.Scope));
            if (!string.IsNullOrEmpty(query.View))
                qsa.Add("v=" + HttpUtility.UrlEncode(query.View));
            if (query.Start > 0)
                qsa.Add("start=" + HttpUtility.UrlEncode(query.Start.ToString()));

            ArrayList ka = new ArrayList();
            if (!string.IsNullOrEmpty(query.Keywords))
                ka.Add(query.Keywords);
            foreach (string property in query.Properties)
            {
                // consider empty facets and don't add them to qs
                if (query.Properties[property] == Constants.UNNAMED_FACET_VALUE) continue;
                // provision for multiple filters for the same facet, delimited by #;#
                if (query.Properties[property].Contains(Splitter.Delimiter))
                {
                    string[] alTmp = query.Properties[property].Split(Splitter.UniDelimiter, StringSplitOptions.RemoveEmptyEntries); ;
                    foreach (string propertyValue in alTmp)
                    {
                        ka.Add(string.Format("{0}:\"{1}\"", property, propertyValue));
                    }
                }
                else
                {
                    ka.Add(string.Format("{0}:\"{1}\"", property, query.Properties[property]));
                }
            }
            if (ka.Count > 0)
            {
                string ks = Splitter.Join(ka, " ");
                qsa.Add("k=" + HttpUtility.UrlEncode(ks));
            }
            if (qsa.Count > 0)
                qs = Splitter.Join(qsa, "&");
            return qs;
        }

        /// <summary>
        /// Builds a querytext by joining META properties with keyword for KeywordQuery
        /// </summary>
        /// <param name="query"><see cref="SearchQuery"/></param>
        /// <returns>Returns a generated querytext string <see cref="string"/> to use in response redirect</returns>
        public static string BuildQueryText(SearchQuery query)
        {
            string qs = string.Empty;
            ArrayList qsa = new ArrayList();

            // keywords
            ArrayList ka = new ArrayList();
            if (!string.IsNullOrEmpty(query.Keywords))
                ka.Add(query.Keywords);

            // scope - be aware of "
            if (!string.IsNullOrEmpty(query.Scope))
                ka.Add(string.Format("scope:\"{0}\"", query.Scope.Trim('"')));

            
            // properties
            if (query.Properties != null)
            {
                foreach (string property in query.Properties)
                {
                    // consider empty facets and don't add them to qs
                    if (query.Properties[property] == Constants.UNNAMED_FACET_VALUE) continue;
                    // provision for multiple filters for the same facet, delimited by #;#
                    if (query.Properties[property].Contains(Splitter.Delimiter))
                    {
                        string[] alTmp = query.Properties[property].Split(Splitter.UniDelimiter, StringSplitOptions.RemoveEmptyEntries); ;
                        foreach (string propertyValue in alTmp)
                        {
                            ka.Add(string.Format("{0}:\"{1}\"", property, propertyValue));
                        }
                    }
                    else
                    {
                        ka.Add(string.Format("{0}:\"{1}\"", property, query.Properties[property]));
                    }
                }
            }
            
            
            if (ka.Count > 0)
            {
                string ks = Splitter.Join(ka, " ");
                qsa.Add(HttpUtility.UrlEncode(ks));

            }
            if (qsa.Count > 0)
                qs = Splitter.Join(qsa, "&");

            return HttpUtility.UrlDecode(qs);
        }

        public static bool IsMatch(string pattern, string value)
        {
            if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(value))
                return false;
            if (pattern.IndexOf('%') != (pattern.Length - 1))
                return (pattern.ToLower() == value.ToLower());  // exact match
            else
                return value.ToLower().StartsWith(pattern.ToLower().TrimEnd('%'));    // starts-with match
        }

        /// <summary>
        /// Decides either to throw a real message or custom one based on debugging state
        /// </summary>
        /// <param name="realMsg"></param>
        /// <param name="friendlyMsg"></param>
        /// <returns></returns>
        public static string ErrorMessage(string realMsg, string friendlyMsg)
        {
#if DEBUG
            return realMsg;
#else
            return friendlyMsg;
#endif
        }

        /// <summary>
        /// Handles exception per EntLib and attempts to display it
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="display"></param>
        /// <param name="friendlyMsg"></param>
        /// <param name="page"></param>
        public static void HandleException(Exception ex, bool display, string friendlyMsg, ControlCollection controls)
        {
            if (display)
            {
                HtmlImage img = new HtmlImage();
                img.EnableViewState = false;
                img.ID = "ErrorIcon";
                img.Src = "/_layouts/images/attention16by16.gif";
                img.Alt = (ex != null) ? ErrorMessage(ex.Message, friendlyMsg) : friendlyMsg;
                controls.AddAt(0, img);

                //Label error = new Label();
                //error.EnableViewState = false;
                //error.CssClass = "ms-error";
                //error.Text = (ex != null) ? ErrorMessage(ex.Message, friendlyMsg) : friendlyMsg;
                //controls.AddAt(1, error);
            }
            if (ex != null && EntLib.ExceptionHandling.ExceptionPolicy.HandleException(ex, Constants.EXCEPTION_POLICY_NAME)) throw ex;
        }

        /// <summary>
        /// Buils HTML string from the control
        /// </summary>
        /// <param name="ctrl"></param>
        /// <returns>string</returns>
        public static string GetHtmlFromControl(Control ctrl)
        {
            StringBuilder result = new StringBuilder(1024);
            ctrl.RenderControl(new HtmlTextWriter(new StringWriter(result)));
            return result.ToString();
        }

        /// <summary>
        /// Workaround custom hash code for the NameValueCollection
        /// </summary>
        /// <param name="col"></param>
        /// <returns></returns>
        public static int GetHashCode(NameValueCollection col)
        {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < col.Count; i++)
            {
                string key = col.Keys[i].ToLower();
                string value = col[i].ToLower();
                builder.Append(key);
                builder.Append("=");
                builder.Append(value);
                builder.AppendLine();
            }
            string prop = builder.ToString();
            return prop.GetHashCode();
        } 
        #endregion
    }

    /// <summary>
    /// The class provides method to split delimited string
    /// </summary>
    internal class Splitter
    {
        #region Fields
        private static Regex reLookup = new Regex(Constants.REGEX_LOOKUP_SPLITTER, RegexOptions.Singleline);
        private static Regex reChoice = new Regex(Constants.REGEX_CHOICE_SPLITTER, RegexOptions.Singleline);
        internal static string Delimiter = "#;#";
        internal static string[] UniDelimiter = new string[] {Delimiter};
        #endregion

        #region Methods
        /// <summary>
        /// Splits a delimited string that comes from value on lookup and choice fields
        /// </summary>
        /// <param name="facetValue"></param>
        /// <returns>array of string values</returns>
        public static string[] Split(string facetValue)
        {
            if (reLookup.IsMatch(facetValue)) return reLookup.Split(facetValue);
            if (reChoice.IsMatch(facetValue)) return reChoice.Split(facetValue);
            return null;
        }

        /// <summary>
        /// Helper method to join array list
        /// </summary>
        /// <param name="al"></param>
        /// <param name="separator"></param>
        /// <returns></returns>
        public static string Join(ArrayList al, string separator)
        {
            string[] ar = (string[])al.ToArray(typeof(string));
            return string.Join(separator, ar);
        }

        /// <summary>
        /// Converts array of Int into array of String
        /// </summary>
        /// <param name="arInt"></param>
        /// <returns></returns>
        public static string[] ConvertToStringArray(int[] arInt)
        {
            string[] arStr = Array.ConvertAll<int, string>(arInt, new Converter<int, string>(ConvertIntToString));
            return arStr;
        }
            

        private static string ConvertIntToString(int i) { return i.ToString(); }
        
        #endregion
    }


}